package ch.bfh.ti.barcode.colorQrCode;

import java.io.UnsupportedEncodingException;
import java.util.EnumMap;

import ch.bfh.ti.barcode.CodeGenerator;
import ch.bfh.ti.barcode.exception.EncodeException;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.google.zxing.qrcode.decoder.Version;

/**
 * This class is able to convert any kind of binary data into a Color QR-Code.
 * The conversion is being made by coloring three black and white barcodes in
 * the colors red, green and blue. Those three barcodes are then stacked on top
 * of each other. It uses the ZXing-Library for the conversation of the black
 * and white QR-Codes.
 * 
 * @author Dominik von Burg
 * 
 */
public class ColorQrCodeGenerator implements CodeGenerator {
	private int width = 0;
	private int height = 0;
	private BarcodeFormat format;
	private MultiFormatWriter writer;
	private int blockSize;
	private EnumMap<EncodeHintType, Object> hints;

	/**
	 * Instantiates a new Color QR-Code object. The size of the barcode has to
	 * be provided and can not be changed during runtime.
	 * 
	 * @param blockSize
	 *            The size of barcode to use for the conversion. This value
	 *            determines the version of QR-Code used. It is also the maximum
	 *            size of the byte array parameter used in the
	 *            {@link #generate(byte[])} method.
	 */
	public ColorQrCodeGenerator(int blockSize) {
		if (blockSize % 3 != 0)
			throw new IllegalArgumentException(
					"Block size has to be dividable by three!");

		this.blockSize = blockSize / 3;

		/*
		 * Try to guess the size of the barcode to set the width and the height.
		 */
		int[] blockSizeInformation = getOptimalBlockSizeInformation();
		int codewords;
		int version = 0;
		for (int i = 0; i < 40; i++) {
			codewords = blockSizeInformation[i];
			if (codewords >= blockSize) {
				version = i + 1;
				break;
			}
		}

		height = width = Version.getVersionForNumber(version)
				.getDimensionForVersion();

		format = BarcodeFormat.QR_CODE;
		writer = new MultiFormatWriter();
		hints = new EnumMap<EncodeHintType, Object>(EncodeHintType.class);
		hints.put(EncodeHintType.CHARACTER_SET, "ISO-8859-1");

		// Set error correction to low / 7%
		hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
	}

	/**
	 * Get the total number of codewords for the version. Subtract the total
	 * number of codewords used for the error correction with the setting L
	 * (low, 7%). Remove the bits for Mode Indicator and Character Count
	 * Indicator.
	 * 
	 * @return An array with the total amount of codewords for a Color QR-Code
	 *         version. Because the array is starting at position zero, the
	 *         first version is located at that position.
	 */
	public static int[] getOptimalBlockSizeInformation() {
		int[] optimalBlockSize = new int[40];
		
		/*
		 * We are sending the byte 31 at the start of every QR-Code to make sure
		 * that ZXing uses the binary encoding for every frame!
		 */
		for (int i = 1; i <= 40; i++)
			optimalBlockSize[i - 1] = ((Version.getVersionForNumber(i)
					.getTotalCodewords()
					- Version.getVersionForNumber(i)
							.getECBlocksForLevel(ErrorCorrectionLevel.L)
							.getTotalECCodewords() - 2) * 3) - 3;
		return optimalBlockSize;
	}

	/**
	 * This method returns the capacity of the code. Its the maximum size of the
	 * byte array you send with the {@link #generate(byte[])} method.
	 * 
	 * @return The capacity of the code.
	 */
	public int getCodeSize() {
		return blockSize * 3;
	}

	/**
	 * This method converts binary data into a Color QR-Code image. The image is
	 * returned as a bitmap in the ARGB format. Any kind of data can be
	 * converted to a barcode. The QR-Code version used for the barcode is
	 * determined by the size provided on instantiating the object. The error
	 * correction level used is L (low).
	 * 
	 * @param source
	 *            Any kind of binary data to convert into a barcode.
	 * @return A image with the Color QR-Code. The image has to be in the ARGB
	 *         format.
	 * @throws EncodeException
	 *             Something went wrong with the conversion or generation of the
	 *             image.
	 */
	public int[] generate(byte[] source) throws EncodeException {
		BitMatrix matrixRed;
		BitMatrix matrixGreen;
		BitMatrix matrixBlue;

		byte[] blockRed = new byte[blockSize + 1];
		System.arraycopy(source, 0, blockRed, 1, blockSize);
		// Force ZXing to use binary encoding
		blockRed[0] = 31;
		String textRed = "";
		try {
			textRed = new String(blockRed, "ISO-8859-1");
		} catch (UnsupportedEncodingException e) {
			throw new EncodeException("Problems with the encoding!");
		}

		byte[] blockGreen = new byte[blockSize + 1];
		System.arraycopy(source, blockSize, blockGreen, 1, blockSize);
		// Force ZXing to use binary encoding
		blockGreen[0] = 31;
		String textGreen = "";
		try {
			textGreen = new String(blockGreen, "ISO-8859-1");
		} catch (UnsupportedEncodingException e) {
			throw new EncodeException("Problems with the encoding!");
		}

		byte[] blockBlue = new byte[blockSize + 1];
		// Force ZXing to use binary encoding
		blockBlue[0] = 31;
		System.arraycopy(source, (blockSize * 2), blockBlue, 1, blockSize);
		String textBlue = "";
		try {
			textBlue = new String(blockBlue, "ISO-8859-1");
		} catch (UnsupportedEncodingException e) {
			throw new EncodeException("Problems with the encoding!");
		}

		try {
			matrixRed = writer.encode(textRed, format, width, height);
			matrixGreen = writer.encode(textGreen, format, width, height);
			matrixBlue = writer.encode(textBlue, format, width, height);
		} catch (WriterException e) {
			throw new EncodeException("ZXing encoding Exception!");
		}

		width = matrixRed.getEnclosingRectangle()[2] + 1;
		height = matrixRed.getEnclosingRectangle()[3] + 1;

		int left = matrixRed.getEnclosingRectangle()[0];
		int top = matrixRed.getEnclosingRectangle()[1];

		int[] pixels = new int[width * height];

		int argb;

		for (int y = 0; y < height; y++) {
			int offset = y * width;
			for (int x = 0; x < width; x++) {
				// Add the value for the alpha channel. We want the picture to
				// be visible!
				argb = 0xff000000;
				// Add full intensity for the red channel if the point is also
				// white on the red barcode.
				if (!matrixRed.get(left + x, top + y))
					argb |= 0x00ff0000;
				// Add full intensity for the green channel if the point is also
				// white on the green barcode.
				if (!matrixGreen.get(left + x, top + y))
					argb |= 0x0000ff00;
				// Add full intensity for the blue channel if the point is also
				// white on the blue barcode.
				if (!matrixBlue.get(left + x, top + y))
					argb |= 0x000000ff;

				/*
				 * Coloring the edges of the barcode. Only a small part is
				 * colored because the colors are difficult to detect. When the
				 * edges are being detected, we get the exact position of them.
				 * It is not difficult to find the colored center.
				 */
				if (x == 3 && y == 3)
					argb = 0xff00ff00;

				if (x == 3 && y == (height - 4))
					argb = 0xff0000ff;

				if (x == (width - 4) && y == 3)
					argb = 0xffff0000;

				pixels[offset + x] = argb;
			}
		}

		return pixels;
	}
}
